package com.ideaaedi.component.dump;

import com.ideaaedi.commonds.bash.BashUtil;
import com.ideaaedi.commonds.exception.ExceptionUtil;
import org.apache.commons.lang3.StringUtils;
import sun.jvm.hotspot.tools.jcore.ClassDump;
import sun.jvm.hotspot.tools.jcore.ClassFilter;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 扩展封装{@link sun.jvm.hotspot.tools.jcore.ClassDump}<br>
 * <br><hr>
 * 特别注意：
 * <ol>
 *     <li>使用此工具类（即{@link sun.jvm.hotspot.tools.jcore.ClassDump}）对目标JVM进行dump class时，如果被dump的类
 *         中有采用invokedynamic机制的代码（提示：部分lambda表达式就采用了该机制），
 *         {@link sun.jvm.hotspot.tools.jcore.ClassDump}是无法采集到此处足够的信息。
 *         这时，对dump出的.class进行反编译，可以看到源码对应的地方使用了&lt;invokedynamic&gt;占位符代替;
 *         此时，可以考虑使用{@link NonExitClassFileTransformerExecutor}来进行dump</li>
 *     <li>使用此工具类（即{@link sun.jvm.hotspot.tools.jcore.ClassDump}）对目标JVM进行dump时，dump期间会使目标JVM stop-the-world</li>
 *     <li>使用此工具类（即{@link sun.jvm.hotspot.tools.jcore.ClassDump}）进行dump时，dump完成时，会退出当前JVM进程（即：程序停止）</li>
 * </ol>
 *
 * <br><hr>
 * 其它说明：<br/>
 * jdk工具sd-jdi.jar里{@link sun.jvm.hotspot.tools.jcore.ClassDump}可以把类的class内容dump到出来. 原生的{@link
 * sun.jvm.hotspot.tools.jcore.ClassDump}默认有两个可选参数：
 * <ul>
 *     <li>sun.jvm.hotspot.tools.jcore.filter： 指定{@link ClassFilter}的类名</li>
 *     <li>sun.jvm.hotspot.tools.jcore.outputDir： class文件输出的目录</li>
 * </ul>
 *
 * <br><hr>
 * 所需依赖：
 * <dependency>
 *      <groupId>sun.jvm.hotspot</groupId>
 *      <artifactId>sa-jdi</artifactId>
 *      <version>${java.version}</version>
 *      <scope>system</scope>
 *      <systemPath>${java.home}/../lib/sa-jdi.jar</systemPath>
 * </dependency>
 *
 * @author JustryDeng
 * @since 2021/9/25 19:40:06
 */
public class ExitDumpClassExecutor {
    
    public static final String ARG_PID = "pid";
    public static final String ARG_INCLUDE_PREFIXES = "includePrefixes";
    public static final String ARG_EXCLUDE_PREFIXES = "excludePrefixes";
    public static final String ARG_OUTPUT_DIR = "outputDir";
    public static final String ARG_KEEP_LONG_NAME = "keepLongName";
    public static final String ARG_VALUE_SEPARATOR = "=";
    
    public static final boolean DEFAULT_KEEP_LONG_NAME = false;
    
    public static final AtomicInteger TRY_TIMES = new AtomicInteger(1);
    
    /**
     * main入口
     */
    public static void main(String[] args){
        try {
            try {
                // ClassDump是否能加载到
                ExitDumpClassExecutor.class.getClassLoader().loadClass("sun.jvm.hotspot.tools.jcore.ClassDump");
            } catch (ClassNotFoundException e) {
                int currTime = TRY_TIMES.getAndIncrement();
                if (currTime > 3) {
                    System.out.println("[ERROR] ClassNotFoundException for 'sun.jvm.hotspot.tools.jcore.ClassDump' over 3 times.");
                    return;
                }
                tryLoadSaJdiAndExecMain(args);
                return;
            }
            // 参数解析
            String pid = parseValueByPrefixFromTail(ARG_PID + ARG_VALUE_SEPARATOR, args);
            String includePrefixes = parseValueByPrefixFromTail(ARG_INCLUDE_PREFIXES + ARG_VALUE_SEPARATOR, args);
            String excludePrefixes = parseValueByPrefixFromTail(ARG_EXCLUDE_PREFIXES + ARG_VALUE_SEPARATOR, args);
            String outputDir = parseValueByPrefixFromTail(ARG_OUTPUT_DIR + ARG_VALUE_SEPARATOR, args);
            String keepLongName = parseValueByPrefixFromTail(ARG_KEEP_LONG_NAME + ARG_VALUE_SEPARATOR, args);
            boolean keepLongNameBool = DEFAULT_KEEP_LONG_NAME;
            try {
                keepLongNameBool = Boolean.parseBoolean(keepLongName);
            } catch (Exception e) {
                // ignore
            }
            exec(pid, includePrefixes, excludePrefixes, outputDir, keepLongNameBool);
        } catch (Throwable e) {
            System.out.println("[ERROR] ExitDumpExecutor#main exception" + ExceptionUtil.getStackTraceMessage(e));
        }
    }
    
    /**
     * @see ExitDumpClassExecutor#exec(String, String, String, String, boolean)
     */
    public static void exec(String pid, String includePrefixes, String outputDir) {
        exec(pid, includePrefixes, null, outputDir, DEFAULT_KEEP_LONG_NAME);
        /*
         * 说明：其实当执行dump时，内部执行完时，最后一步就会执行sun.jvm.hotspot.tools.Tool#execute，进而退出程序，根本不会执行到这一行。
         *       之所以还多余的显示的写个System.exit(0)，是为了让人一眼就能看知道这个方法执行完后，程序会退出，慎用
         */
        System.exit(0);
    }
    
    /**
     * @see ExitDumpClassExecutor#exec(String, String, String, String, boolean)
     */
    public static void exec(String pid, String includePrefixes, String outputDir, boolean keepLongName) {
        exec(pid, includePrefixes, null, outputDir, keepLongName);
        /*
         * 说明：其实当执行dump时，内部执行完时，最后一步就会执行sun.jvm.hotspot.tools.Tool#execute，进而退出程序，根本不会执行到这一行。
         *       之所以还多余的显示的写个System.exit(0)，是为了让人一眼就能看知道这个方法执行完后，程序会退出，慎用
         */
        System.exit(0);
    }
    
    /**
     * dump 指定JVM中的class
     *
     * @param pid
     *         要被进行dump操作的JVM进程id
     * @param includePrefixes
     *         要dump的class的全类名前缀，多个使用逗号分割
     * @param excludePrefixes
     *         明确排除dump的class的全类名前缀，多个使用逗号分割
     * @param outputDir
     *            输出目录
     * @param keepLongName
     *            是否以全类名作为文件名
     *            <br><br>
     *            假设现有类com.example.HelloWorld，那么：
     *            <ul>
     *                <li>为true时：输出的文件为com.example.HelloWorld.class</li>
     *                <li>为false时：：输出的文件为com/example/HelloWorld/class</li>
     *            </ul>
     *
     *
     */
    public static void exec(String pid, String includePrefixes, String excludePrefixes,
                            String outputDir, boolean keepLongName) {
        System.out.printf("pid -> %s\nincludePrefixes -> %s\nexcludePrefixes -> %s\noutputDir -> %s\nkeepLongName -> %s\n",
                pid, includePrefixes, excludePrefixes, outputDir, keepLongName);
        // ClassDump.main(new String[]{pid});执行到最后一步时，方法内部会主动调用exit退出当前JVM进程（即：结束程序），这里添加钩子，在程序结束前进行统计输出
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.printf("dumped file count %s, paths：\n", PrefixMatchClassFilter.DUMP_FILE_PATH_LIST.size());
                for (int i = 1; i <= PrefixMatchClassFilter.DUMP_FILE_PATH_LIST.size(); i++) {
                    System.out.println(i + ". " + PrefixMatchClassFilter.DUMP_FILE_PATH_LIST.get(i - 1));
                }
            }
        }));
        
        // step1. 校验并设置相关参数
        /*
         * 不能dump当前JVM的class, 要不然ClassDump.main时会报错:
         * Error attaching to process: Windbg Error: AttachProcess failed!
         * sun.jvm.hotspot.debugger.DebuggerException: Windbg Error: AttachProcess failed!
         * 	at sun.jvm.hotspot.debugger.windbg.WindbgDebuggerLocal.attach0(Native Method)
         */
        if (BashUtil.currPid().equals(pid)) {
            throw new IllegalArgumentException("includePrefixes cannot be blank.");
        }
        // includePrefixes
        if (StringUtils.isBlank(includePrefixes)) {
            throw new IllegalArgumentException("includePrefixes cannot be blank.");
        }
        System.setProperty(PrefixMatchClassFilter.INCLUDE_PREFIXES_KEY, includePrefixes);
        // excludePrefixes
        if (StringUtils.isNotBlank(excludePrefixes)) {
            System.setProperty(PrefixMatchClassFilter.EXCLUDE_PREFIXES_KEY, excludePrefixes);
        }
        // outputDir
        if (StringUtils.isBlank(outputDir)) {
            throw new IllegalArgumentException("outputDir cannot be blank.");
        }
        System.setProperty(PrefixMatchClassFilter.OUTPUT_DIR_KEY, outputDir);
        // keepLongName
        System.setProperty(PrefixMatchClassFilter.KEEP_LONG_NAME_KEY, String.valueOf(keepLongName));
        
        // step2. 使用自定义的ClassFilter，实现扩展
        System.setProperty(PrefixMatchClassFilter.CLASS_DUMP_FILTER_KEY, PrefixMatchClassFilter.class.getTypeName());
        
        /*
         * step3. 触发dump
         * 特别注意：ClassDump.main(new String[]{pid});的执行逻辑中，在最后一步时，会System.exit()退出程序结束当前进程。
         * 所以：你在ClassDump.main(new String[]{pid});后面写的代码是不会执行的。
         */
        ClassDump.main(new String[]{pid});
        /*
         * 说明：其实当执行dump时，内部执行完时，最后一步就会执行sun.jvm.hotspot.tools.Tool#execute，进而退出程序，根本不会执行到这一行。
         *       之所以还多余的显示的写个System.exit(0)，是为了让人一眼就能看知道这个方法执行完后，程序会退出，慎用
         */
        System.exit(0);
    }

    /**
     * 根据前缀解析值
     * <p>
     *     如: 数组中某元素的为 k1=v1, 此方法传入的前缀为k1=， 那么此方法会返回v1
     * </p>
     *
     * @param prefix
     *            前缀
     * @param args
     *            参数数组
     * @return  解析出来的值，若没有则返回null
     */
    private static String parseValueByPrefixFromTail(String prefix, String[] args) {
        Objects.requireNonNull(prefix, "prefix cannot be null.");
        if (args == null) {
            return null;
        }
        // 从后往前找，即: java -jar k1=v1 k2=v2 k3=v3 -xxx.jar中，若k冲突了，那么取后面的k对应的v
        for (int i = args.length - 1; i >= 0; i--) {
            if (args[i] == null || StringUtils.isBlank(args[i])) {
                continue;
            }
            args[i] = args[i].trim();
            if (args[i].startsWith(prefix)) {
                return args[i].substring(prefix.length());
            }
        }
        return null;
    }
    
    /**
     * 试着再加载sa-jdi.jar，并执行{@link ExitDumpClassExecutor#main}
     *
     * @param mainArgs
     *            {@link ExitDumpClassExecutor#main}的入参
     */
    private static void tryLoadSaJdiAndExecMain(String[] mainArgs) throws
            MalformedURLException, ClassNotFoundException, NoSuchMethodException,
            IllegalAccessException, InvocationTargetException {
        System.out.println("can not find sa-jdi.jar from classpath, try to load it from java.home.");
        String javaHome = System.getProperty("java.home");
        if (javaHome == null) {
            javaHome = System.getenv("JAVA_HOME");
        }
        if (javaHome == null) {
            System.out.println("can not get java.home, can not load sa-jdi.jar.");
            System.exit(-1);
        }
        
        File saJdiJar = new File(javaHome + "/lib/sa-jdi.jar");
        if (!saJdiJar.exists()) {
            // java.home maybe jre
            saJdiJar = new File(javaHome + "/../lib/sa-jdi.jar");
            if (!saJdiJar.exists()) {
                System.out.println("can not find lib/sa-jdi.jar from java.home: " + javaHome);
            }
        }
        // build a new classloader, a trick.
        List<URL> urls = new ArrayList<>(Arrays.asList(((URLClassLoader) ExitDumpClassExecutor.class.getClassLoader()).getURLs()));
        urls.add(saJdiJar.toURI().toURL());
        
        URLClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader().getParent());
        Class<?> startClass = classLoader.loadClass(ExitDumpClassExecutor.class.getName());
        final Method exitDumpExecutorMain = startClass.getMethod("main", String[].class);
        if (!exitDumpExecutorMain.isAccessible()) {
            exitDumpExecutorMain.setAccessible(true);
        }
        exitDumpExecutorMain.invoke(null, new Object[] {mainArgs});
    }
}
